1 /**
2 This module provides a function for pretty-printing D arrays of various dimensions.
3 A multidimensional array is represented as a 2D matrix surrounded by nested square frames.
4 If the array is too big, it will be truncated accordingly.
5 The surrounding frame, and the truncation symbol can be changed as well as truncation options.
6 */
7 module pretty_array;
8 
9 import std.array : join, array;
10 import std.conv : to;
11 import std.utf : byCodeUnit;
12 import std.typecons : tuple, Tuple;
13 import mir.ndslice;
14 
15 /// TODO: a placeholder string for NaNs
16 enum NAN = "nan";
17 /// TODO: a placeholder string for Infs
18 enum INF = "inf";
19 
20 /// pretty_array formatting configuration.
21 private enum Format : int
22 {
23     edgeitems = 3, // sets N leading and trailing items for each dimension
24     threshold = 300, // max N array elements allowed without truncation
25     precision = 8, // TODO: precision of floating point representations
26     suppressExp = 0, // TODO: suppress printing small floating values in exp format
27     lineWidth = 120
28 }
29 
30 private enum Frame : string
31 {
32     ltAngle = "┌",
33     lbAngle = "└",
34     rtAngle = "┐",
35     rbAngle = "┘",
36     vBar = "│",
37     newline = "\n",
38     dash = "─",
39     dot = "·",
40     space = " ",
41     truncStr = "░" // TIP: length of this string is 3!
42 }
43 
44 ulong[] getShape(T : int)(T obj, ulong[] dims = null)
45 {
46     return dims;
47 }
48 
49 ulong[] getShape(T : double)(T obj, ulong[] dims = null)
50 {
51     return dims;
52 }
53 
54 /++
55 Get the shape of a plain D array.
56 A standalone convenience function for getting array shape without converting to Mir Slices.
57 The array must have correct dimensions otherwise the column index will not be consistent.
58 +/
59 ulong[] getShape(T)(T obj, ulong[] dims = null)
60 in
61 {
62     import std.traits : isArray;
63 
64     assert(isArray!(typeof(obj)));
65 }
66 do
67 {
68     dims ~= obj.length.to!int;
69     return getShape!(typeof(obj[0]))(obj[0], dims);
70 }
71 
72 // Calculate the length of array elements converted to strings.
73 private ulong getStrLength(T)(T arrSlice)
74 {
75     if (arrSlice.shape.length == 1)
76     {
77         return arrSlice.map!(a => a.to!string).join.length;
78     }
79     else
80     {
81         auto slice2D = arrSlice.flattened.chunks(arrSlice.shape[$ - 1]);
82         return slice2D[0].map!(a => a.to!string).join.length;
83     }
84 }
85 
86 // Convert truncated index to real array index.
87 private ulong convertTruncIdx(ulong idx, ulong truncLen, ulong rowLen)
88 {
89     pragma(inline, true);
90     return idx > Format.edgeitems ? rowLen - (truncLen - idx) : idx;
91 }
92 
93 /++
94 Get the longest string length of a row, construct a row with the longest string elements.
95 We need to know the longest string length of the row to calculate the correct padding between the frames.
96 We need to keep the row with longest string elements to correctly right-align all array elements.
97 +/
98 private Tuple!(ulong, "strlen", string[], "row") getMaxStrLenAndMaxRow(T)(T arrSlice, bool truncate)
99 {
100 
101     auto slice2D = arrSlice.flattened.chunks(arrSlice.shape[$ - 1]);
102     const ulong truncLen = Format.edgeitems * 2 + 1;
103     const bool enoughRows = slice2D.shape[0] > truncLen;
104     const bool encoughCols = slice2D[0].length > truncLen;
105     ulong maxStrRowLen;
106     string[] row, maxRow;
107 
108     // fill the empty array with the number of row elements
109     // there probably a better way to do it
110     for (int i; i < (truncate && encoughCols ? truncLen : slice2D[0].length); i++)
111     {
112         maxRow ~= "0";
113         row ~= "";
114     }
115 
116     // construct a row with longest string elements
117     ulong rowi, colj;
118     for (ulong i; i < (truncate && enoughRows ? truncLen : slice2D.shape[0]); i++)
119     {
120         rowi = truncate && enoughRows ? convertTruncIdx(i, truncLen, slice2D.shape[0]) : i;
121         for (ulong j; j < (truncate && encoughCols ? truncLen : slice2D[rowi].length);
122                 j++)
123         {
124             colj = truncate && encoughCols ? convertTruncIdx(j, truncLen, slice2D[i].length) : j;
125             row[j] = slice2D[rowi][colj].to!string;
126         }
127 
128         for (ulong k; k < row.length; k++)
129         {
130             if (truncate && encoughCols && (k == Format.edgeitems))
131             {
132                 maxRow[k] = Frame.truncStr;
133                 continue;
134             }
135             maxRow[k] = maxRow[k].length < row[k].length ? row[k] : maxRow[k];
136         }
137     }
138     maxStrRowLen = truncate && encoughCols
139         ? maxRow.join.length + truncLen - 3 : maxRow.join.length + slice2D[0].length - 1; // -3 because Frame.truncStr.length == 3
140     return Tuple!(ulong, "strlen", string[], "row")(maxStrRowLen, maxRow);
141 }
142 
143 /++
144 Construct the padding between frame angles.
145 Use white space if padding string is not provided.
146 +/
147 private string getPadding(T)(T arrShape, ulong maxStrRowLen, string padStr = Frame.space)
148 {
149     return padStr.byCodeUnit.repeat((arrShape.length < 2
150             ? 0 : arrShape.length - 2) * 2 + maxStrRowLen).join;
151 }
152 
153 private ulong lenDiff()(string a, string b)
154 {
155     return a.length > b.length ? a.length - b.length : 0;
156 }
157 
158 private string prettyFrame(T)(T arrSlice, bool truncate)
159         if (arrSlice.shape.length == 1)
160 {
161     if (truncate)
162     {
163         string[] leftSlice = arrSlice[0 .. Format.edgeitems].map!(a => a.to!string).array;
164         string[] rightSlice = arrSlice[$ - Format.edgeitems .. $].map!(a => a.to!string).array;
165         return Frame.vBar ~ (leftSlice ~ Frame.truncStr ~ rightSlice)
166             .join(" ") ~ Frame.vBar ~ Frame.newline;
167     }
168     else
169     {
170 
171         return Frame.vBar ~ arrSlice.map!(a => a.to!string).join(" ") ~ Frame.vBar ~ Frame.newline;
172     }
173 
174 }
175 
176 private string prettyFrame(T)(T arrSlice, string addedFrame, Tuple!(ulong,
177         "strlen", string[], "row") maxRow, bool truncate)
178         if (arrSlice.shape.length == 2)
179 {
180     string arrStr;
181     ulong rowi, colj;
182     const ulong truncLen = Format.edgeitems * 2 + 1;
183     const bool enoughRows = arrSlice.shape[0] > truncLen;
184     const bool enoughCols = arrSlice.shape[1] > truncLen;
185 
186     for (ulong i; i < (truncate && enoughRows ? truncLen : arrSlice.shape[0]); i++)
187     {
188         string[] newRow;
189         rowi = truncate && enoughRows ? convertTruncIdx(i, truncLen, arrSlice.length) : i;
190         for (ulong j; j < (truncate && enoughCols ? truncLen : arrSlice[rowi].length);
191                 j++)
192         {
193             colj = truncate && enoughCols ? convertTruncIdx(j, truncLen, arrSlice[i].length) : j;
194             // insert white spaces before the element to right align it
195             newRow ~= " ".repeat(lenDiff(maxRow.row[j],
196                     arrSlice[rowi][colj].to!string)).join ~ arrSlice[rowi][colj].to!string;
197 
198             if (truncate && enoughCols && (j == Format.edgeitems))
199             {
200                 newRow[$ - 1] = Frame.truncStr; // overwrite last with truncation string
201             }
202         }
203 
204         if (truncate && enoughRows)
205         {
206             if (i != Format.edgeitems)
207                 arrStr ~= addedFrame ~ newRow.join(" ") ~ addedFrame ~ Frame.newline;
208             else
209                 arrStr ~= addedFrame ~ (cast(string) Frame.truncStr)
210                     .repeat(maxRow.strlen).join ~ addedFrame ~ Frame.newline;
211         }
212         else
213         {
214             arrStr ~= addedFrame ~ newRow.join(" ") ~ addedFrame ~ Frame.newline;
215         }
216     }
217     return arrStr;
218 }
219 
220 private string prettyFrame(T)(T arrSlice, string addedFrame, Tuple!(ulong,
221         "strlen", string[], "row") maxRow, bool truncate)
222         if (arrSlice.shape.length > 2)
223 {
224     string arrStr;
225     for (ulong i; i < arrSlice.shape[0]; i++)
226     {
227         string padding = getPadding!(typeof(arrSlice[i].shape))(arrSlice[i].shape, maxRow.strlen);
228         arrStr ~= addedFrame ~ Frame.ltAngle ~ padding ~ Frame.rtAngle ~ addedFrame ~ Frame.newline;
229         arrStr ~= prettyFrame!(typeof(arrSlice[i]))(arrSlice[i],
230                 addedFrame ~ Frame.vBar, maxRow, truncate);
231         arrStr ~= addedFrame ~ Frame.lbAngle ~ padding ~ Frame.rbAngle ~ addedFrame ~ Frame.newline;
232     }
233 
234     return arrStr;
235 }
236 
237 // Check if an array can be truncated.
238 private bool canTruncate(T)(T arrSlice)
239 {
240     return (arrSlice.flattened.length > Format.threshold) || ((arrSlice.shape.length == 1)
241             && (arrSlice.getStrLength > Format.lineWidth)) ? true : false;
242 }
243 
244 /++
245 Pretty-print D array.
246 +/
247 string prettyArr(T)(T arr)
248 in
249 {
250     assert(isConvertibleToSlice!(typeof(arr)));
251 }
252 do
253 {
254     string arrStr;
255     auto arrSlice = arr.fuse; // convert to Mir Slice by GC allocating with fuse
256     // check if we need array truncation
257     const bool truncate = arrSlice.canTruncate;
258     auto maxRow = arrSlice.getMaxStrLenAndMaxRow(truncate);
259     string padding = getPadding!(typeof(arrSlice.shape))(arrSlice.shape, maxRow.strlen);
260     arrStr ~= Frame.ltAngle ~ padding ~ Frame.rtAngle ~ Frame.newline;
261     static if (arrSlice.shape.length > 1)
262     {
263         arrStr ~= prettyFrame!(typeof(arrSlice))(arrSlice, Frame.vBar, maxRow, truncate);
264     }
265     else
266     {
267         arrStr ~= prettyFrame!(typeof(arrSlice))(arrSlice, truncate);
268     }
269     arrStr ~= Frame.lbAngle ~ padding ~ Frame.rbAngle ~ Frame.newline;
270     return arrStr;
271 }
272 
273 unittest
274 {
275     import std.range : chunks;
276 
277     // TODO: getShape tests
278 
279     int[] a0 = [200, 1, -3, 0, 0, 8501, 23];
280     string testa0 = "┌                    ┐
281 │200 1 -3 0 0 8501 23│
282 └                    ┘
283 ";
284     assert(prettyArr!(typeof(a0))(a0) == testa0);
285 
286     auto a = [5, 2].iota!int(1).fuse;
287     auto maxa = a.getMaxStrLenAndMaxRow(a.canTruncate);
288     assert(getPadding!(typeof(a.shape))(a.shape, maxa.strlen).length == 4);
289     string testa = "┌    ┐
290 │1  2│
291 │3  4│
292 │5  6│
293 │7  8│
294 │9 10│
295 └    ┘
296 ";
297     assert(prettyArr!(typeof(a))(a) == testa);
298 
299     auto b = [2, 2, 6].iota!int(1).fuse;
300     auto maxb = b.getMaxStrLenAndMaxRow(b.canTruncate);
301     assert(getPadding!(typeof(b.shape))(b.shape, maxb.strlen).length == 19);
302     string testb = "┌                   ┐
303 │┌                 ┐│
304 ││ 1  2  3  4  5  6││
305 ││ 7  8  9 10 11 12││
306 │└                 ┘│
307 │┌                 ┐│
308 ││13 14 15 16 17 18││
309 ││19 20 21 22 23 24││
310 │└                 ┘│
311 └                   ┘
312 ";
313     assert(prettyArr!(typeof(b))(b) == testb);
314     int[] carr = [
315         1000, 21, 1232, 4, 5, 36, 1207, 18, 9, 10, -1, 12, 133, -14, 21915, 16
316     ];
317     auto c = carr.chunks(2).array.chunks(4).array.chunks(2).array; // jagged D array
318     string testc = "┌             ┐
319 │┌           ┐│
320 ││┌         ┐││
321 │││ 1000  21│││
322 │││ 1232   4│││
323 │││    5  36│││
324 │││ 1207  18│││
325 ││└         ┘││
326 ││┌         ┐││
327 │││    9  10│││
328 │││   -1  12│││
329 │││  133 -14│││
330 │││21915  16│││
331 ││└         ┘││
332 │└           ┘│
333 └             ┘
334 ";
335     assert(prettyArr!(typeof(c))(c) == testc);
336 
337     auto d = [3, 1, 2, 1].iota!int(1).fuse;
338     string testd = "┌     ┐
339 │┌   ┐│
340 ││┌ ┐││
341 │││1│││
342 │││2│││
343 ││└ ┘││
344 │└   ┘│
345 │┌   ┐│
346 ││┌ ┐││
347 │││3│││
348 │││4│││
349 ││└ ┘││
350 │└   ┘│
351 │┌   ┐│
352 ││┌ ┐││
353 │││5│││
354 │││6│││
355 ││└ ┘││
356 │└   ┘│
357 └     ┘
358 ";
359     assert(prettyArr!(typeof(d))(d) == testd);
360 
361     auto e = [2, 3, 6, 6].iota!int(1).fuse;
362     string teste = "┌                           ┐
363 │┌                         ┐│
364 ││┌                       ┐││
365 │││  1   2   3   4   5   6│││
366 │││  7   8   9  10  11  12│││
367 │││ 13  14  15  16  17  18│││
368 │││ 19  20  21  22  23  24│││
369 │││ 25  26  27  28  29  30│││
370 │││ 31  32  33  34  35  36│││
371 ││└                       ┘││
372 ││┌                       ┐││
373 │││ 37  38  39  40  41  42│││
374 │││ 43  44  45  46  47  48│││
375 │││ 49  50  51  52  53  54│││
376 │││ 55  56  57  58  59  60│││
377 │││ 61  62  63  64  65  66│││
378 │││ 67  68  69  70  71  72│││
379 ││└                       ┘││
380 ││┌                       ┐││
381 │││ 73  74  75  76  77  78│││
382 │││ 79  80  81  82  83  84│││
383 │││ 85  86  87  88  89  90│││
384 │││ 91  92  93  94  95  96│││
385 │││ 97  98  99 100 101 102│││
386 │││103 104 105 106 107 108│││
387 ││└                       ┘││
388 │└                         ┘│
389 │┌                         ┐│
390 ││┌                       ┐││
391 │││109 110 111 112 113 114│││
392 │││115 116 117 118 119 120│││
393 │││121 122 123 124 125 126│││
394 │││127 128 129 130 131 132│││
395 │││133 134 135 136 137 138│││
396 │││139 140 141 142 143 144│││
397 ││└                       ┘││
398 ││┌                       ┐││
399 │││145 146 147 148 149 150│││
400 │││151 152 153 154 155 156│││
401 │││157 158 159 160 161 162│││
402 │││163 164 165 166 167 168│││
403 │││169 170 171 172 173 174│││
404 │││175 176 177 178 179 180│││
405 ││└                       ┘││
406 ││┌                       ┐││
407 │││181 182 183 184 185 186│││
408 │││187 188 189 190 191 192│││
409 │││193 194 195 196 197 198│││
410 │││199 200 201 202 203 204│││
411 │││205 206 207 208 209 210│││
412 │││211 212 213 214 215 216│││
413 ││└                       ┘││
414 │└                         ┘│
415 └                           ┘
416 ";
417     assert(e.prettyArr == teste);
418 
419     auto f = [100, 5].iota!int(1).fuse;
420     string testf = "┌                   ┐
421 │  1   2   3   4   5│
422 │  6   7   8   9  10│
423 │ 11  12  13  14  15│
424 │░░░░░░░░░░░░░░░░░░░│
425 │486 487 488 489 490│
426 │491 492 493 494 495│
427 │496 497 498 499 500│
428 └                   ┘
429 ";
430 
431     auto g = [100, 100].iota!int(1).fuse;
432     string testg = "┌                                ┐
433 │   1    2    3 ░   98   99   100│
434 │ 101  102  103 ░  198  199   200│
435 │ 201  202  203 ░  298  299   300│
436 │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
437 │9701 9702 9703 ░ 9798 9799  9800│
438 │9801 9802 9803 ░ 9898 9899  9900│
439 │9901 9902 9903 ░ 9998 9999 10000│
440 └                                ┘
441 ";
442 
443     auto h = [500].iota!int(1).fuse;
444     string testh = "┌                   ┐
445 │1 2 3 ░ 498 499 500│
446 └                   ┘
447 ";
448     assert(h.prettyArr == testh);
449 
450     auto i = [2, 100, 500].iota!int(1).fuse;
451     string testi = "┌                                        ┐
452 │┌                                      ┐│
453 ││    1     2     3 ░   498   499    500││
454 ││  501   502   503 ░   998   999   1000││
455 ││ 1001  1002  1003 ░  1498  1499   1500││
456 ││░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░││
457 ││48501 48502 48503 ░ 48998 48999  49000││
458 ││49001 49002 49003 ░ 49498 49499  49500││
459 ││49501 49502 49503 ░ 49998 49999  50000││
460 │└                                      ┘│
461 │┌                                      ┐│
462 ││50001 50002 50003 ░ 50498 50499  50500││
463 ││50501 50502 50503 ░ 50998 50999  51000││
464 ││51001 51002 51003 ░ 51498 51499  51500││
465 ││░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░││
466 ││98501 98502 98503 ░ 98998 98999  99000││
467 ││99001 99002 99003 ░ 99498 99499  99500││
468 ││99501 99502 99503 ░ 99998 99999 100000││
469 │└                                      ┘│
470 └                                        ┘
471 ";
472 
473 }